package com.ntd.study.algorithm.reduce.L77T;

/**
 * Class for performing LZ77 compression/decompression using text IO
 *
 * @author Mark Hall
 * @version 1.0
 */
public class LZ77T {
  private final int MAX_WND_SIZE = 1024;
  private final int OFFSET_CODING_LENGTH = 10;
  private final int constM = 3;

  private byte[] pOutputBuffer;
  private int pulNumberOfBytes;

  public LZ77T() {
    pOutputBuffer=new byte[4000000];
    pulNumberOfBytes=0;

  }

  public void lz77decompress(byte[] pDataBuffer,
                             int ulNumberOfBits) {
    int iSlideWindowPtr = 0;
    int offset;
    int offset2;
    int length = 0, wndOffset = 0;
    int bit = 0;
    byte cc = 0;
    int i = 0;
    int ulBytesDecoded = 0;
    int ulBitOffset = 0;
    ReturnData returnData = new ReturnData();

    int ulCodingLength = 0;

    iSlideWindowPtr = -MAX_WND_SIZE;

    ulBitOffset = 0;
    ulBytesDecoded = 0;
    offset2 = 0;
    offset = 0;

    while (ulBitOffset < ulNumberOfBits) {
      bit = ReadBitFromBitStream(pDataBuffer, ulBitOffset);
      ulBitOffset++;

      if (bit == 1) {

        if (iSlideWindowPtr >= 0) {
          offset = iSlideWindowPtr;

        }
        else if (iSlideWindowPtr >= -MAX_WND_SIZE) {
          offset = 0;
        }
        else {
          offset = 0;
        }

        for (i = 0, wndOffset = 0; i < OFFSET_CODING_LENGTH; i++, ulBitOffset++) {
          bit = ReadBitFromBitStream(pDataBuffer, ulBitOffset);
          wndOffset |= (bit << i);
        }

        returnData = ReadGolombCode(pDataBuffer, ulBitOffset);

        length = returnData.getByteNum();

        ulCodingLength = returnData.getBitNum();

        wndOffset = wndOffset % MAX_WND_SIZE;

        if (length > MAX_WND_SIZE) {
          length = length % MAX_WND_SIZE;
        }
        ulBitOffset += ulCodingLength;

        for (i = 0; i < length; i++) {
          pOutputBuffer[offset2 + i] = pOutputBuffer[offset + wndOffset];
          offset++;
        }

        offset2 += length;

        iSlideWindowPtr += length;
        ulBytesDecoded += length;
      }
      else {
        for (i = 0, cc = 0; i < 8; i++, ulBitOffset++) {
          bit = ReadBitFromBitStream(pDataBuffer, ulBitOffset);
          cc |= ( (byte) bit << i);
        }
        pOutputBuffer[offset2] = cc;

        offset2++;

        iSlideWindowPtr++;
        ulBytesDecoded++;
      }

    }
    pulNumberOfBytes = ulBytesDecoded;

    setPulNumberOfBytes(pulNumberOfBytes);
    setPOutputBuffer(pOutputBuffer);
  }

  private int ReadBitFromBitStream(byte[] pBuffer, int ulBitOffset) {
    int ulByteBoundary = 0;
    int ulOffsetInByte = 0;

    ulByteBoundary = (int) (ulBitOffset >> 3);
    ulOffsetInByte = ulBitOffset & 7;

    return ( ( (byte) (pBuffer[ulByteBoundary] >> ulOffsetInByte)) &
            ( (byte) 0x01));
  }

  private ReturnData ReadGolombCode(byte[] pBuffer, int ulBitOffset) {
    int q, r;
    int bit = 0;
    int i;
    int pulCodingLength;
    ReturnData returnData = new ReturnData();

    for (q = 0; ; q++) {
      bit = ReadBitFromBitStream(pBuffer, ulBitOffset);
      ulBitOffset++;
      if (bit != 1) {
        break;
      }
    }

    for (i = 0, r = 0; (int) i < constM; i++, ulBitOffset++) {
      bit = (int) ReadBitFromBitStream(pBuffer, ulBitOffset);
      bit <<= i;
      r |= bit;
    }

    pulCodingLength = constM + q + 1;

    returnData.setBitNum(pulCodingLength);
    returnData.setByteNum(r + (q << constM) + 1);

    return returnData;
  }

  public byte[] getPOutputBuffer() {
    return pOutputBuffer;
  }

  public int getPulNumberOfBytes() {
    return pulNumberOfBytes;
  }

  private void setPOutputBuffer(byte[] pOutputBuffer) {
    this.pOutputBuffer = pOutputBuffer;
  }

  private void setPulNumberOfBytes(int pulNumberOfBytes) {
    this.pulNumberOfBytes = pulNumberOfBytes;
  }

}
